using System;
using System.Collections.Generic;
using System.Text;

namespace csammisrun.MsnLib.Utility
{
  /// <summary>
  /// Describes the general type of a switchboard
  /// </summary>
  public enum SwitchboardType
  {
    /// <summary>
    /// This switchboard is used for conversations
    /// </summary>
    Conversation,
    /// <summary>
    /// This switchboard is used for file transfers
    /// </summary>
    FileTransfer
  }

  /// <summary>
  /// Describes the specific type of a switchboard
  /// </summary>
  public enum SwitchboardSubType
  {
    /// <summary>
    /// This switchboard is being used in a 1-1 conversation
    /// </summary>
    InstantMessage,
    /// <summary>
    /// This switchboard is being used in a 1-many conversation
    /// </summary>
    ChatRoom
  }

  public enum SwitchboardError
  {
    None,
    CantJoinConversation,
    SelfOffline,
    OtherOffline,
    Connection,
    SendingTooFast,
    Unknown
  }

  /// <summary>
  /// So I says to Mabel, I says...
  /// </summary>
  public class Switchboard
  {
    private const string FILETRANSFERGUID = "{5D3E02AB-6190-11d3-BBBB-00C04F795683}";
    private const string AVTRANSFERGUID = "{02D3C01F-BF30-4825-A83A-DE7AF41648AA}";

    internal static int MasterID = 0;

    private SwitchboardType _type = SwitchboardType.Conversation;
    private SwitchboardSubType _subtype = SwitchboardSubType.InstantMessage;
    private Session _parent = null;
    private ServerConnection _conn = null;
    private CommandProcessor _proc = null;
    private string _imuser = "";
    private string _authkey = "";
    private string _sessionid = "";
    private bool _isempty = true;
    private bool _hasbeeninvited = false;
    private bool _isready = false;
    private bool _hasbeenclosed = false;
    private bool _isdying = false;
    private int _currentusers = 0;
    private int _totalusers = 0;
    private int _id = 0;
    private List<User> _users = new List<User>();
    private Queue<Message> _messagequeue = new Queue<Message>();
    private List<Message> _waitingforack = new List<Message>();
    private List<string> _userstoinvite = new List<string>();

    public Switchboard(Session parent, SwitchboardType type)
    {
      _id = MasterID++;
      _type = type;
      _parent = parent;
      _conn = new ServerConnection(parent, ServerConnectionType.SwitchBoard);
      _proc = _conn.CommandProc;
      _proc.Data = this;
      _parent.Switches.Add(this);

      InitProcessorRegistry();
    }

    #region Switchboard connection routines
    /// <summary>
    /// Initiate a request for a new switchboard
    /// </summary>
    public void Connect()
    {
      Transaction trans = new Transaction(_proc, "XFR", "{0}", "SB");
      trans.Data = this;
      _parent.NotificationBoard.Processor.SendTransaction(trans);
    }

    internal void ConnectToServer(string server, int port)
    {
      _conn.Connected += new ServerConnectionEventHandler(_conn_Connected);
      _conn.Disconnected += new ServerConnectionEventHandler(_conn_Disconnected);
      _conn.ConnectionLost += new ServerConnectionErrorHandler(_conn_ConnectionLost);
      _conn.Connect(server, port);
    }

    void _conn_ConnectionLost(ServerConnection conn, string error)
    {
      
    }

    void _conn_Disconnected(ServerConnection conn)
    {
      
    }

    void _conn_Connected(ServerConnection conn)
    {
      if (this.HasBeenInvited)
      {
        _isempty = false;
        _isready = true;
        _proc.SendCommand("ANS", "{0} {1} {2}", _parent.Username, this.AuthKey, this.SessionID);
      }
      else
      {
        _isready = true;
        _proc.SendCommand("USR", "{0} {1}", _parent.Username, this.AuthKey);
      }
    }
    #endregion

    /// <summary>
    /// Disconnects from the switchboard
    /// </summary>
    public void Disconnect()
    {
      if (_conn.IsConnected)
      {
        // Send an OUT message directly through the connection
        Logging.WriteString("Sending OUT for switchboard");
        _conn.SendPacket(Encoding.UTF8.GetBytes("OUT\r\n"));
        Logging.WriteString("Disconnecting switchboard");
        _conn.Disconnect();
      }
    }

    /// <summary>
    /// Adds a user to the switchboard
    /// </summary>
    public void AddUser(string passport)
    {
      User user = _parent.Userlist.FindUserByPassport(passport);
      _users.Add(user);
      _currentusers++;
      _isempty = false;

      // Extra processing for chatrooms
      if (_currentusers > 1)
      {
        if (_subtype == SwitchboardSubType.InstantMessage)
        {
          // Well *balls*, we need to convert this noise to a chatroom
          _subtype = SwitchboardSubType.ChatRoom;
          ChatRoom newchat = new ChatRoom(this);
          _parent.Chatrooms.Add(newchat);
          // Alert the parent of a chat room creation
        }

        _parent.OnChatRoomUserJoined(_parent.FindChatroomBySwitchboard(this), user);
      }
    }

    /// <summary>
    /// Sends a message to the switchboard indicating the client capabilities
    /// </summary>
    /// <remarks>If the switchboard is not in a state in which it can send messages, the client capabilities
    /// are not sent.  Unlike text messages, it is not queued.</remarks>
    public void SendClientCapabilities()
    {
      if (CanSendMessages)
      {
        Message msg = new Message(MessageType.Capabilities);
        msg.ContentType = "text/x-clientcaps";
        msg.Flag = 'U';
        string caps = "Client-Name: MsnLib/" + Session.MSNLIB_VERSION + "\r\nChat-Logging: ";
        caps += (_parent.IsClientLogging) ? "Y" : "N";
        caps += "\r\n";
        msg.SetBinaryData(Encoding.UTF8.GetBytes(caps));
        SendMessage(msg);
      }
    }

    /// <summary>
    /// Flushes the message queue immediately
    /// </summary>
    public void ProcessQueue()
    {
      if (!CanSendMessages)
      {
        return;
      }
      lock (_messagequeue)
      {
        while (_messagequeue.Count > 0)
        {
          // Make DAMN SURE that it doesn't get added into the queue at this point
          SendMessageNow(_messagequeue.Dequeue());
        }
      }
    }

    /// <summary>
    /// Send a message through the switchboard
    /// </summary>
    /// <param name="msg">The <see cref="Message"/> to send</param>
    /// <remarks>This method will queue the message if the switchboard is not in a state in which a message can be sent</remarks>
    public void SendMessage(Message msg)
    {
      if (CanSendMessages)
      {
        SendMessageNow(msg);
      }
      else
      {
        lock (_messagequeue)
        {
          _messagequeue.Enqueue(msg);
        }
      }
    }

    /// <summary>
    /// Sends a typing notification to the switchboard
    /// </summary>
    /// <remarks>If the switchboard is not in a state in which it can send messages, the typing notification
    /// is not sent.  Unlike text messages, it is not queued.</remarks>
    public void SendTypingNotification()
    {
      if (CanSendMessages)
      {
        Message msg = new Message(MessageType.Typing);
        msg.SetAttribute("TypingUser", _parent.Username);
        SendMessageNow(msg);
      }
    }

    /// <summary>
    /// Invite a user to the conversation
    /// </summary>
    public void InviteUser(string passport)
    {
      Transaction trans = new Transaction(_proc, "CAL", "{0}", passport);
      _proc.SendTransaction(trans);
    }

    #region Command handlers

    private void InitProcessorRegistry()
    {
      CommandRegistrar cr = _proc.Registry;
      cr.RegisterHandlerForServerResponse("ANS", "ANS", new CommandHandler(AnsCmd));
      cr.RegisterHandlerForServerResponse("ANS", "IRO", new CommandHandler(IroCmd));
      cr.RegisterHandlerForServerResponse("MSG", "ACK", new CommandHandler(AckCmd));
      cr.RegisterHandlerForServerResponse("MSG", "NAK", new CommandHandler(NakCmd));
      cr.RegisterHandlerForServerResponse("USR", "USR", new CommandHandler(UsrCmd));

      cr.RegisterHandlerForServerCommand("MSG", new CommandHandler(MsgCmd));
      cr.RegisterHandlerForServerCommand("JOI", new CommandHandler(JoiCmd));
      cr.RegisterHandlerForServerCommand("BYE", new CommandHandler(ByeCmd));
      cr.RegisterHandlerForServerCommand("OUT", new CommandHandler(OutCmd));

      cr.RegisterHandlerForError("MSG", new CommandErrorHandler(MsgErr));
      cr.RegisterHandlerForError("CAL", new CommandErrorHandler(CalErr));

      cr.RegisterHandlerForMessage("text/plain", new MessageHandler(PlainMsg));
      cr.RegisterHandlerForMessage("text/x-msmsgscontrol", new MessageHandler(ControlMsg));
      cr.RegisterHandlerForMessage("text/x-clientcaps", new MessageHandler(ClientMsg)); // Not implemented in gaim
      cr.RegisterHandlerForMessage("text/x-clientinfo", new MessageHandler(ClientMsg)); // Not implemented in gaim
      cr.RegisterHandlerForMessage("application/x-msnmsgrp2p", new MessageHandler(P2PMsg));
      cr.RegisterHandlerForMessage("text/x-mms-emoticon", null);
      cr.RegisterHandlerForMessage("text/x-mms-aniemoticon", null);
      cr.RegisterHandlerForMessage("text/x-msnmsgr-datacast", new MessageHandler(NudgeMsg));
      cr.RegisterHandlerForMessage("text/x-msmmsginvite", new MessageHandler(TransferMsg));
    }

    private void AnsCmd(CommandProcessor processor, MsnCommand command)
    {
      _isready = true;
    }

    private void IroCmd(CommandProcessor processor, MsnCommand command)
    {
      Int32.TryParse(command.Parameters[2], out _totalusers);
      AddUser(command.Parameters[3]);
    }

    private void AckCmd(CommandProcessor processor, MsnCommand command)
    {
    }

    private void NakCmd(CommandProcessor processor, MsnCommand command)
    {
    }

    private void UsrCmd(CommandProcessor processor, MsnCommand command)
    {
      // Gets our own passport and friendly name after a sign in...fantastic
      foreach (string passport in _userstoinvite)
      {
        InviteUser(passport);
      }
      _userstoinvite.Clear();
      // Gaim flushes the message queue here...why, I don't know
    }

    private void MsgCmd(CommandProcessor processor, MsnCommand command)
    {
      //MsnMessage msg = MsnMessage.CreateMessageFromCommand(command);
      //if (msg != null)
      //{
      //  msg.ProcessSwitchboardMessage(this);
      //}

      Message msg = new Message(_parent, command);
      msg.ParsePayload(command.Payload);
      msg.RemoteUser = command.Parameters[0];
      processor.ProcessMessage(msg);
    }

    private void JoiCmd(CommandProcessor processor, MsnCommand command)
    {
      AddUser(command.Parameters[0]);

      ProcessQueue();

      if (!_parent.UseHTTP)
      {
        SendClientCapabilities();
      }

      if (this.HasBeenClosed)
      {
        // Close this bugger
      }
    }

    private void ByeCmd(CommandProcessor processor, MsnCommand command)
    {
      User user = GetSwitchboardUser(command.Parameters[0]);

      if (user != null)
      {
        if (this.SubType == SwitchboardSubType.ChatRoom)
        {
          ChatRoom cr = _parent.FindChatroomBySwitchboard(this);
          if(cr != null)
          {
            _users.Remove(user);
            _parent.OnChatRoomUserLeft(cr, user);
            if (_users.Count == 0)
            {
              _parent.Switches.Remove(this);
              this.Disconnect();
            }
          }
        }
        else
        {
          _parent.Switches.Remove(this);
          this.Disconnect();
        }
      }
      else
      {
        Logging.WriteString("Received a BYE command on a switchboard for a user that isn't here: " + command.Parameters[0]);
      }
    }

    private void OutCmd(CommandProcessor processor, MsnCommand command)
    {
    }
    #endregion

    #region Message handlers
    private void PlainMsg(CommandProcessor processor, Message msg)
    {
      msg.Type = MessageType.Text;
      User user = GetSwitchboardUser(msg.RemoteUser);

      if (user != null)
      {
        string html = Message.TextMessageToHTML(msg);
        if (this.SubType == SwitchboardSubType.InstantMessage)
        {
          _parent.OnMessageReceived(user, html);
        }
        else
        {
          ChatRoom chatroom = _parent.FindChatroomBySwitchboard(this);
          _parent.OnChatRoomMessageReceived(chatroom, user, html);
        }
      }
      else
      {
        Logging.WriteString("Received a message for a user that isn't here: " + msg.RemoteUser);
      }
    }

    private void ControlMsg(CommandProcessor processor, Message msg)
    {
      User user = GetSwitchboardUser(msg.RemoteUser);
      if (user != null)
      {
        string value = msg.Attributes("TypingUser");
        if (!String.IsNullOrEmpty(value))
        {
          if (SubType == SwitchboardSubType.InstantMessage)
          {
            _parent.OnTypingNotificationReceived(user);
          }
          else
          {
            _parent.OnChatRoomTypingNotificationReceived(_parent.FindChatroomBySwitchboard(this), user);
          }
        }
        else
        {
          Logging.WriteString(String.Format("Received unhandled control message from {0}:\r\n{1}", msg.RemoteUser, msg.ToString()));
        }
      }
    }

    private void ClientMsg(CommandProcessor processor, Message msg)
    {
      // No action
    }

    private void NudgeMsg(CommandProcessor processor, Message msg)
    {
      User user = GetSwitchboardUser(msg.RemoteUser);
      if (user != null)
      {
        _parent.OnNudgeReceived(user);
      }
      else
      {
        Logging.WriteString("Received a nudge from a user who isn't here: " + msg.RemoteUser);
      }
    }

    private void P2PMsg(CommandProcessor processor, Message msg)
    {
      P2PMessage message = new P2PMessage(this, msg);
      if (message.ProcessMessage())
      {
        
      }
    }

    private void TransferMsg(CommandProcessor processor, Message msg)
    {
      User user = GetSwitchboardUser(msg.RemoteUser);
      if (user == null)
      {
        Logging.WriteString("Received a transfer message from a user not in the switchboard: " + msg.RemoteUser);
        return;
      }

      Dictionary<string, string> fields = msg.GetFieldsFromBody();
      string guid = fields["Application-GUID"];
      switch (fields["Invitation-Command"].ToUpper())
      {
        case "INVITE":
          if (guid == FILETRANSFERGUID)
          {
            _parent.OnFileTransferInvitationReceived(user,
              fields["Application-File"], Int32.Parse(fields["Application-FileSize"]),
              fields["Invitation-Cookie"]);
          }
          else if (guid == AVTRANSFERGUID)
          {
          }
          else
          {
            Logging.WriteString("Received unknown transfer guid: " + guid);
            return;
          }
          break;
        case "ACCEPT":
          break;
        case "CANCEL":
          // Check to see if this cookie belongs to an AV transfer list, otherwise it's a file transfer
          _parent.OnFileTransferCancelled(fields["Invitation-Cookie"], fields["Cancel-Code"]);
          break;
      }
    }
    #endregion

    #region Error handlers
    private void MsgErr(CommandProcessor processor, Transaction trans, int error)
    {
      Logging.WriteString("Got message error " + error + " on switchboard " + _id.ToString());
    }

    private void CalErr(CommandProcessor processor, Transaction trans, int error)
    {
      Logging.WriteString("Got CAL error " + error + " on switchboard " + _id.ToString());
    }
    #endregion

    #region Properties
    /// <summary>
    /// Gets or sets the session ID for this switchboard
    /// </summary>
    public string SessionID
    {
      get { return _sessionid; }
      set { _sessionid = value; }
    }

    /// <summary>
    /// Gets or sets a value indicating whether or not we were invited to this switchboard
    /// </summary>
    public bool HasBeenInvited
    {
      get { return _hasbeeninvited; }
      set { _hasbeeninvited = value; }
    }

    public string IMUser
    {
      get { return _imuser; }
      set { _imuser = value; }
    }

    /// <summary>
    /// Gets or sets this switchboard's authorization key
    /// </summary>
    public string AuthKey
    {
      get { return _authkey; }
      set { _authkey = value; }
    }

    /// <summary>
    /// Gets or sets a value indicating whether or not this switchboard has been closed
    /// </summary>
    public bool HasBeenClosed
    {
      get { return _hasbeenclosed; }
      set { _hasbeenclosed = value; }
    }

    /// <summary>
    /// Gets a value indicating whether or not the switchboard is unoccupied
    /// </summary>
    public bool IsEmpty
    {
      get { return _isempty; }
    }

    /// <summary>
    /// Gets the <see cref="SwitchboardType"/> of this switchboard
    /// </summary>
    public SwitchboardType Type
    {
      get { return _type; }
    }

    /// <summary>
    /// Gets the <see cref="SwitchboardSubType"/> of this switchboard
    /// </summary>
    public SwitchboardSubType SubType
    {
      get { return _subtype; }
      set { _subtype = value; }
    }

    /// <summary>
    /// Gets a list of <see cref="User"/>s on the switchboard
    /// </summary>
    public List<User> Participants
    {
      get { return _users; }
    }

    /// <summary>
    /// Gets a list of users to invite once the switchboard is connected
    /// </summary>
    public List<string> UsersToInvite
    {
      get { return _userstoinvite; }
    }

    /// <summary>
    /// Gets a value indicating whether a message can be sent immediately,
    /// or whether it must be queued
    /// </summary>
    private bool CanSendMessages
    {
      get
      {
        return _conn.IsConnected && !IsEmpty;
      }
    }

    /// <summary>
    /// Gets the <see cref="Session"/> that owns the switchboard
    /// </summary>
    public Session ParentSession
    {
      get { return _parent; }
    }
    #endregion

    #region Utility methods
    /// <summary>
    /// Creates a transaction from a message and sends it to the switchboard
    /// </summary>
    private void SendMessageNow(Message msg)
    {
      Transaction trans = null;
      byte[] payload = null;

      payload = msg.GeneratePayload();
      trans = new Transaction(_proc, "MSG", "{0} {1}", msg.Flag, payload.Length);
      trans.Data = msg;
      // Tomfoolery with the message timeouts, if I want to do that
      trans.Payload = payload;
      msg.Transaction = trans;
      _proc.SendTransaction(trans);
    }

    /// <summary>
    /// Returns a <see cref="User"/> object belonging to the current <see cref="Switchboard"/>
    /// </summary>
    public User GetSwitchboardUser(string passport)
    {
      User user = null;
      foreach (User u in _users)
      {
        if (u.Passport == passport)
        {
          user = u;
          break;
        }
      }
      return user;
    }
    #endregion

    public void SendFileTransferAck(string sid)
    {
      P2PInfo info = P2PMessage.GetInfoBySID(sid);
      if (info == null)
      {
        return;
      }

      bool newmsgid = false;
      if (info.MsgID == 0)
      {
        info.GenerateMsgID();
        newmsgid = true;
      }
      else
      {
        info.MsgID++;
      }

      Message msg = new Message(MessageType.SLP);
      msg.SetAttribute("P2P-Dest", info.Destination);
      
      //set b [binary format ii $originalsid $MsgId][binword 0][binword $originalsize][binary format iiii 0 2 $originalid $originaluid][binword $originalsize][binary format I 0]

    }

    public void SendFileTransferCancel(string sid)
    {
      P2PInfo info = P2PMessage.GetInfoBySID(sid);
      string body = "SessionID: " + sid + "\r\n\r\n\0";
      if (info.MsgID == 0)
      {
        info.GenerateMsgID();
      }

      StringBuilder data = new StringBuilder("MSNSLP/1.0 603 Decline\r\n");
      data.AppendFormat("To: <msnmsgr:{0}>\r\n", info.Destination);
      data.AppendFormat("From: <msnmsgr:{0}>\r\n", _parent.Username);
      data.Append("Via: MSNSLP/1.0/TLP ;branch={" + info.BranchID + "}\r\n");
      data.AppendFormat("CSeq: 1\r\n");
      data.Append("Call-ID: {" + info.CallID + "}\r\n");
      data.AppendFormat("Max-Forwards: 0\r\n");
      data.Append("Content-Type: application/x-msnmsgr-sessionreqbody\r\n");
      data.AppendFormat("Content-Length: {0}\r\n\r\n", Encoding.UTF8.GetByteCount(body));
      data.Append(body);

      Message msg = new Message(MessageType.SLP);
      msg.SetAttribute("P2P-Dest", info.Destination);
      msg.SetBinaryData(Encoding.UTF8.GetBytes(data.ToString()));
      msg.Header.MsgID = info.MsgID;
      msg.Header.Offset = info.Offset;
      msg.Header.TotalSize = info.TotalDataSize;
      this.SendMessage(msg);
      P2PMessage.RemoveInfoBySID(sid);
    }
  }
}
